package cheesymock;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class proveds static utility methods for working with methods,
 * properties and exceptions.
 *
 * @author fredrik
 */
public class MethodUtil {
	private static final Pattern GETTER = Pattern.compile("(is|get)[A-Z].*");
	private static final Pattern SETTER = Pattern.compile("set[A-Z].*");
	private static final Pattern PROPERTY = Pattern.compile("(is|get|set)([A-Z].*)");

	private MethodUtil() {
	}

	/**
	 * Produces a print-friendly list of argument types.
	 *
	 * @argument arguments an array of {@link Object}s representing
	 *    methods arguments.
	 * @return a string containing the argument list.
	 */
	public static String argList(Object[] arguments) {
		StringBuilder b = new StringBuilder();
		if (arguments != null) {
			for (Object arg : arguments) {
				if (b.length() > 0) {
					b.append(",");
				}
				if (arg == null) {
					b.append("Object");
				} else {
					b.append(arg.getClass().getName());
				}
			}
		}
		return b.toString();
	}

	/**
	 * Produces a print-friendly string with a method signature
	 * (method name and argument types).
	 *
	 * @argument method the method to be printed
	 * @argument arguments an array of {@link Object}s representing
	 *    methods arguments.
	 * @return a string containing the method signature.
	 */
	public static String methodName(Method method, Object[] arguments) {
		return new StringBuilder().append(method.getName()).append("(").append(argList(arguments)).append(")")
			.toString();
	}

	/**
	 * Checks if two classes are equivalent; i.e. if the first is assignable
	 * from the other, or if the first is the primitive version (for example int
	 * instead of Integer) of the other.
	 * 
	 * @param cl1
	 * @param cl2
	 * @return true if the classes are equivalent
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	public static boolean typeIsEquivalent(Class<?> cl1, Class<?> cl2) throws SecurityException,
		IllegalArgumentException, IllegalAccessException {
		if (!cl1.isPrimitive() && cl2 == null) {
			return true;
		}
		if (cl1.isAssignableFrom(cl2)) {
			return true;
		} else if (cl1.isPrimitive()) {
			try {
				Field typeField = cl2.getField("TYPE");
				if (cl1.equals(typeField.get(cl2))) {
					return true;
				}
			} catch (NoSuchFieldException e) {
			}
		}
		return false;
	}

	/**
	 * Converts an array of method arguments to an array of argument
	 * types by repeatedly calling {@link Object#getClass}.
	 *
	 * @argument arguments an array of {@link Object}s representing
	 *    method arguments. Passing in a null value is okay, and
	 *    returns a zero-element array.
	 * @return an array of {@link Class}es representing the
	 *    corresponding argument types.
	 */
	public static Class<?>[] argumentTypes(Object[] arguments) {
		if (arguments == null) {
			return new Class<?>[0];
		}
		Class<?>[] signature = new Class<?>[arguments.length];
		for (int i = 0; i < arguments.length; i++) {
			signature[i] = arguments[i] == null ? null : arguments[i].getClass();
		}
		return signature;
	}

	/**
	 * Resolve 'method' to a method in the delegate object.
	 * 
	 * @throws NoSuchMethodException
	 *    if a suitable method can not be found.
	 */
	public static Method resolveMethod(Method method, Class<?>[] signature, Object delegate)
		throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException {
		String name = method.getName();
		Class<?> returnType = method.getReturnType();
		if (delegate != null) {
			Method[] methods = delegate.getClass().getDeclaredMethods();
			for (Method m : methods) {
				if (name.equals(m.getName()) && m.getReturnType().equals(returnType)
						&& Arrays.equals(signature, m.getParameterTypes())) {
					return m;
				}
			}
			for (Method m : methods) {
				if (name.equals(m.getName()) && m.getReturnType().equals(returnType)) {
					Class<?>[] parameterTypes = m.getParameterTypes();
					if (parameterTypes.length == signature.length) {
						boolean found = true;
						for (int j = 0; j < parameterTypes.length; j++) {
							if (!typeIsEquivalent(parameterTypes[j], signature[j])) {
								found = false;
								break;
							}
						}
						if (found) {
							return m;
						}
					}
				}
			}
		}
		for(Method m: Object.class.getDeclaredMethods()) {
			if (name.equals(m.getName()) && m.getReturnType().equals(returnType)
					&& Arrays.equals(signature, m.getParameterTypes())) {
				return m;
			}
		}
		throw new NoSuchMethodException();
	}

	/**
	 * Resolve 'method' to a method in the delegate object.
	 * 
	 * @throws NoSuchMethodException
	 *             if a suitable method can not be found.
	 */
	public static Method resolveMethod(Method method, Object[] arguments, Object delegate)
		throws NoSuchMethodException, SecurityException, IllegalArgumentException, IllegalAccessException {
		return MethodUtil.resolveMethod(method, argumentTypes(arguments), delegate);
	}

	public static Field resolveProperty(Method method, Class<?>[] signature, Object delegate)
		throws NoSuchFieldException {
		if ((!isGetter(method, signature)) && (!isSetter(method, signature))) {
			throw new NoSuchFieldException();
		}
		Matcher m = PROPERTY.matcher(method.getName());
		if (m.matches()) {
			String n = m.group(2);
			return delegate.getClass().getDeclaredField(n.substring(0, 1).toLowerCase() + n.substring(1));
		}
		throw new NoSuchFieldException();
	}

	public static Field resolveProperty(Method method, Object[] arguments, Object delegate) throws NoSuchFieldException {
		return resolveProperty(method, argumentTypes(arguments), delegate);
	}

	/**
	 * Determines if a given method is a property getter by looking at
	 * the method name and arguments.
	 *
	 * @return true if the argument list is empty, the name starts
	 *    with either 'is' or 'get' followed by a capital letter and
	 *    has a return type other than void.
	 */
	public static boolean isGetter(Method method, Object[] signature) {
		if ((signature != null && signature.length > 0) || Void.TYPE.equals(method.getReturnType())) {
			return false;
		}
		return GETTER.matcher(method.getName()).matches();
	}

	/**
	 * Determines if a given method is a property setter by looking at
	 * the method name and arguments.
	 * 
	 * @return true if the argument list contains exactly one element,
	 *    name starts with 'set' followed by a capital letter and the
	 *    return type is void.
	 */
	public static boolean isSetter(Method method, Object[] signature) {
		if (signature == null || signature.length != 1 || !Void.TYPE.equals(method.getReturnType())) {
			return false;
		}
		return SETTER.matcher(method.getName()).matches();
	}

	/**
	 * Takes a {@link Throwable} and converts it to a {@link
	 * RuntimeException} by doing some creative wrapping and unwrapping.
	 * <ol>
	 * <li>If the root cause of the Throwable is a RuntimeException, return that.</li>
	 * <li>If the Throwable is itself a RuntimeException, then return that instead.</li>
	 * <li>Otherwise, wrap the Throwable in a new RuntimeException and return that.</li>
	 * </ol>
	 */
	public static RuntimeException toRuntimeException(Throwable t) {
		Throwable c = t.getCause();
		if (c != null && c instanceof RuntimeException) {
		    throw (RuntimeException) c;
		}
		if (t instanceof RuntimeException) {
		    throw (RuntimeException) t;
		}
		throw new RuntimeException(t);
	}
}